/*
 * Copyright (C) 2010 TopCoder Inc., All Rights Reserved.
 */
package com.hercules.xre.protocol.testing.handlers;

import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import com.hercules.xre.protocol.testing.EventHandlerException;
import com.hercules.xre.protocol.testing.ProtocolTestingConnectionManager;
import com.hercules.xre.protocol.testing.ProtocolTestingConnectionManagerException;

/**
 * This class represents command sending event handler. It provides feature to read commands from CommandRetriever
 * interface and send the commands to ProtocolTestingConnectionManager. It extends BaseEventHandler to provide logging.
 * <p>
 *
 * <pre>
 * // create instance using default constructor
 * CommandSendingEventHandler instance = new CommandSendingEventHandler();
 *
 * // if we need logging, we can set logger by name, directly
 * instance.setLoggerName(&quot;test&quot;);
 * instance.setLogger(LogManager.getLog(&quot;test&quot;));
 *
 * // now we need to set command retriever, we can use the one, implemented in this component
 * FileCommandRetriever commandRetriever = new FileCommandRetriever();
 * commandRetriever.setFilename(&quot;test_files/sample.txt&quot;);
 * instance.setCommandRetriever(commandRetriever);
 *
 * // also we need to set protocol testing connection manager
 * instance.setProtocolTestingConnectionManager(new MyManager());
 *
 * // now we can call handleConnectionXXX methods
 * // this methods will send corresponding commands retrieved by commandRetriver using connection manager
 * try {
 *     instance.handleConnectionCreated(&quot;event&quot;);
 *     instance.handleConnectionEvent(&quot;guid&quot;, &quot;event&quot;);
 *     instance.handleConnectionShutdown(&quot;guid&quot;, &quot;event&quot;);
 *
 *     // this command only validate arguments
 *     instance.handleConnectionDisconnected(&quot;guid&quot;, new ArrayList&lt;String&gt;());
 * } catch (EventHandlerException e) {
 *     // handle the error
 *     e.printStackTrace();
 * }
 * </pre>
 *
 * <b>Thread safety:</b> This class is effectively thread safe (see restriction below). It synchronizes on the methods
 * that would write to file. This class will be thread safe if getter and setter are be called before calling any other
 * method.
 *
 * @author bramandia, 7realm
 * @version 1.0
 */
public class CommandSendingEventHandler extends BaseEventHandler {
    /** Represents command index command part. */
    private static final String COMMAND_INDEX_COMMAND_PART = "\"commandIndex\":";
    /** Represents timestamp command part. */
    private static final String TIMESTAMP_COMMAND_PART = "\"timestamp\":";
    /** Represents pattern to parse session GUID. */
    private static final Pattern PATTERN_SESSION_GUID = Pattern.compile("\"sessionGUID\"\\:\\s*\"(.*?)\"");
    /**
     * Represents the protocol testing connection manager to be used. Initialized to null. Has getter/setter. Will not
     * be null after setter. It is used in all methods.
     */
    private ProtocolTestingConnectionManager protocolTestingConnectionManager;
    /**
     * Represents the command retriever to be used. Initialized to null. Has getter/setter. Will not be null after
     * setter. It is used in all methods.
     */
    private CommandRetriever commandRetriever;

    /**
     * Empty default constructor.
     */
    public CommandSendingEventHandler() {
        // empty
    }

    /**
     * Gets the protocol testing connection manager.
     *
     * @return the protocol testing connection manager
     */
    public ProtocolTestingConnectionManager getProtocolTestingConnectionManager() {
        String methodName = "getProtocolTestingConnectionManager";
        Helper.logEnter(getLogger(), methodName);
        return Helper.logExit(getLogger(), methodName, protocolTestingConnectionManager);
    }

    /**
     * Sets the protocol testing connection manager.
     *
     * @param protocolTestingConnectionManager the new value of the protocol testing connection manager
     * @throws IllegalArgumentException if argument is null
     */
    public void setProtocolTestingConnectionManager(ProtocolTestingConnectionManager protocolTestingConnectionManager) {
        String methodName = "setProtocolTestingConnectionManager";

        try {
            // log enter and check argument
            Helper.logEnterAndCheck(getLogger(), methodName, "protocolTestingConnectionManager",
                    protocolTestingConnectionManager);
            this.protocolTestingConnectionManager = protocolTestingConnectionManager;

            Helper.logExit(getLogger(), methodName);
        } catch (IllegalArgumentException e) {
            throw Helper.logIAE(getLogger(), e);
        }
    }

    /**
     * Gets the command retriever.
     *
     * @return the command retriever
     */
    public CommandRetriever getCommandRetriever() {
        String methodName = "getCommandRetriever";
        Helper.logEnter(getLogger(), methodName);
        return Helper.logExit(getLogger(), methodName, commandRetriever);
    }

    /**
     * Sets the command retriever.
     *
     * @param commandRetriever the new value of the command retriever
     * @throws IllegalArgumentException if argument is null
     */
    public void setCommandRetriever(CommandRetriever commandRetriever) {
        String methodName = "setCommandRetriever";

        try {
            // log enter and check argument
            Helper.logEnterAndCheck(getLogger(), methodName, "commandRetriever", commandRetriever);
            this.commandRetriever = commandRetriever;

            Helper.logExit(getLogger(), methodName);
        } catch (IllegalArgumentException e) {
            throw Helper.logIAE(getLogger(), e);
        }
    }

    /**
     * Handle the connection created.
     *
     * @param event the event to be handled, must not be null/empty
     * @throws IllegalArgumentException if any argument is invalid
     * @throws IllegalStateException if commandRetriever or protocolTestingConnectionManager are not set
     * @throws SessionGUIDNotFoundException if the session GUID can't be found
     * @throws EventHandlerException if any error occurred (wrap any other exception)
     */
    public void handleConnectionCreated(String event) throws EventHandlerException {
        try {
            handleEvent(EventType.CONNECTION_CREATED, "handleConnectionCreated", null, "event", event);
        } catch (SessionGUIDNotFoundException e) {
            throw Helper.logError(getLogger(), "Cannot find session GUID for event '" + event + "'.", e);
        }
    }

    /**
     * Handle the connection event.
     *
     * @param sessionGUID the unique session GUID for the application, must not be null/empty
     * @param event the event to be handled, must not be null/empty
     * @throws IllegalArgumentException if any argument is invalid
     * @throws IllegalStateException if commandRetriever or protocolTestingConnectionManager are not set
     * @throws EventHandlerException if any error occurred (wrap any other exception)
     */
    public void handleConnectionEvent(String sessionGUID, String event) throws EventHandlerException {
        handleEvent(EventType.CONNECTION_EVENT, "handleConnectionEvent", sessionGUID, "sessionGUID", sessionGUID,
                "event", event);
    }

    /**
     * Handle the connection disconnected.
     *
     * @param sessionGUID the unique session GUID for the application, must not be null/empty
     * @param unsentCommands the list of unsentCommand strings to be handled, must not be null nor contain null/empty
     *            element
     * @throws EventHandlerException this exception never be thrown from this method
     * @throws IllegalArgumentException if any argument is invalid
     * @throws IllegalStateException if commandRetriever or protocolTestingConnectionManager are not set
     */
    public void handleConnectionDisconnected(String sessionGUID, List<String> unsentCommands)
        throws EventHandlerException {
        handleEvent(EventType.CONNECTION_DISCONNECTED, "handleConnectionDisconnected", sessionGUID, "sessionGUID",
                sessionGUID, "unsentCommands", unsentCommands);
    }

    /**
     * Handle the connection shutdown.
     *
     * @param sessionGUID the unique session GUID for the application, must not be null/empty
     * @param event the event to be handled, must not be null/empty
     * @throws IllegalArgumentException if any argument is invalid
     * @throws IllegalStateException if commandRetriever or protocolTestingConnectionManager are not set
     * @throws EventHandlerException if any error occurred (wrap any other exception)
     */
    public void handleConnectionShutdown(String sessionGUID, String event) throws EventHandlerException {
        handleEvent(EventType.CONNECTION_SHUTDOWN, "handleConnectionShutdown", sessionGUID, "sessionGUID",
                sessionGUID, "event", event);
    }

    /**
     * Handle the connection event. Send corresponding commands.
     *
     * @param eventType the type of the event
     * @param methodName the name of calling method
     * @param sessionGuid the session GUID, null means that it should be fetched from command
     * @param params the parameters of calling method
     * @throws IllegalArgumentException if arguments are incorrect
     * @throws IllegalStateException if commandRetriever or protocolTestingConnectionManager are not set
     * @throws ScreenshotDifferException if the screen shot differs on comparison
     * @throws EventHandlerException if any error occurred (wrap any other exception)
     */
    private void handleEvent(EventType eventType, String methodName, String sessionGuid, Object... params)
        throws EventHandlerException {
        try {
            // log enter and check argument
            Helper.logEnterAndCheck(getLogger(), methodName, params);

            // check state
            Helper.checkState(commandRetriever != null, "The command retriever is not set.");
            Helper.checkState(protocolTestingConnectionManager != null,
                    "The protocolTestingConnectionManager is not set.");

            if (eventType != EventType.CONNECTION_DISCONNECTED) {
                List<String> commands = commandRetriever.retrieveCommands(eventType);
                String guid = null;
                for (String command : commands) {
                    if (guid == null) {
                        guid = getGUID(sessionGuid, command);
                    }
                    command = populateCommand(command, TIMESTAMP_COMMAND_PART);
                    command = populateCommand(command, COMMAND_INDEX_COMMAND_PART);
                    protocolTestingConnectionManager.sendCommand(guid, command);
                    Helper.log(getLogger(), "Command '" + command + "' was populated and sent for event type '"
                            + eventType + "', session GUID: '" + guid + "'.");
                }
            }
            Helper.logExit(getLogger(), methodName);
        } catch (IllegalArgumentException e) {
            throw Helper.logIAE(getLogger(), e);
        } catch (IllegalStateException e) {
            throw Helper.logISE(getLogger(), e);
        } catch (CommandRetrieverException e) {
            String message = "Error occurred while retrieving the command.";
            throw Helper.logError(getLogger(), message, new EventHandlerException(message, e));
        } catch (ProtocolTestingConnectionManagerException e) {
            String message = "Error occurred while sending the command.";
            throw Helper.logError(getLogger(), message, new EventHandlerException(message, e));
        }
    }

    /**
     * Populate the command with time stamp and command index properties.
     *
     * @param command the command to populate
     * @param commandPart the command part, that need to be present
     * @return the populated command
     */
    private static String populateCommand(String command, String commandPart) {
        int index = 0;
        while (true) {
            index = command.indexOf(commandPart, index);
            if (index == -1) {
                break;
            }
            // check count of double quotes
            int count = 0;
            int quoteIndex = 0;
            while (true) {
                quoteIndex = command.indexOf('"', quoteIndex);
                if (quoteIndex == -1 || quoteIndex > index) {
                    break;
                }
                quoteIndex++;
                count++;
            }
            if (count % 2 == 0) {
                return command;
            }
            index++;
        }
        // replace first curly brace with command part
        return command.replaceFirst("\\{", "{" + commandPart + "0,").trim();
    }

    /**
     * Get session GUID from command or use provided.
     *
     * @param sessionGUID the provided session GUID, can be null
     * @param command the command, from where session GUID will be fetched if it is null
     * @return the fetched or provided session GUID
     * @throws SessionGUIDNotFoundException if session GUID cannot be fetched from command
     */
    private static String getGUID(String sessionGUID, String command) throws SessionGUIDNotFoundException {
        // if method arguments has session guid, then return it
        if (sessionGUID != null) {
            return sessionGUID;
        }

        // try to find session guid
        Matcher matcher = PATTERN_SESSION_GUID.matcher(command);
        if (!matcher.find()) {
            throw new SessionGUIDNotFoundException("Session GUID cannot be found in command '" + command + "'.");
        }

        // if pattern matched, then session guid is in first capturing group
        return matcher.group(1);
    }
}
